namespace SpacetimeDB.BSATN;

using System.Text;

/// <summary>
/// Implemented by product types marked with [SpacetimeDB.Type].
/// All rows in SpacetimeDB are product types, so this is also implemented by all row types.
/// </summary>
public interface IStructuralReadWrite
{
    /// <summary>
    /// Initialize this value from the reader.
    /// The reader is assumed to store a <see href="https://spacetimedb.com/docs/bsatn">BSATN-encoded</see>
    /// value of this type.
    /// Advances the reader to the first byte past the end of the read value.
    /// Throws an exception if this would advance past the end of the reader.
    /// </summary>
    /// <param name="reader"></param>
    void ReadFields(BinaryReader reader);

    /// <summary>
    /// Write the fields of this type to the writer.
    /// Throws an exception if the underlying writer throws.
    /// Throws if this value is malformed (i.e. has null values for fields that
    /// are not explicitly marked nullable.)
    /// </summary>
    /// <param name="writer"></param>
    void WriteFields(BinaryWriter writer);

    /// <summary>
    /// Get an IReadWrite implementation that can read values of this type.
    /// In Rust, this would return <c>IReadWrite&lt;Self&gt;</c>, but the C# type system
    /// has no equivalent Self type -- that is, you can't use the implementing type in type signatures
    /// in an interface. So, you have to manually downcast.
    /// A typical invocation looks like: <c>(IReadWrite&lt;Row&gt;) new Row().GetSerializer()</c>
    ///
    /// This is an instance method because of limitations of C# interfaces.
    /// (C# 11 has static virtual interface members, but Unity does not support C# 11.)
    /// This method always works, whether or not the row it is called on is correctly initialized.
    /// The returned serializer has nothing to do with the row GetSerializer is called on -- it returns
    /// new rows and does not modify or interact with the original row.
    ///
    /// Using the resulting serializer rather than <c>Read&lt;T&gt;</c> is usually faster in Mono/IL2CPP.
    /// This is because we manually monomorphise the code to read rows in our automatically-generated
    /// implementation of IReadWrite. This allows rows to be initialized with new() rather than reflection
    /// in the compiled code.
    /// </summary>
    /// <returns>An <c>IReadWrite&lt;T&gt;</c> for <c>T : IStructuralReadWrite</c>.</returns>
    object GetSerializer();

    /// <summary>
    /// Read a row from the reader.
    /// This method usually uses Activator.CreateInstance to create the resulting row;
    /// if this is too slow, prefer using GetSerializer.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="reader"></param>
    /// <returns></returns>
    static T Read<T>(BinaryReader reader)
        where T : IStructuralReadWrite, new()
    {
        // TODO: use `RuntimeHelpers.GetUninitializedObject` as an optimisation here.
        // We tried but couldn't do this because BitCraft relies on being able
        // to add and initialize custom fields on autogenerated classes.
        var result = new T();
        result.ReadFields(reader);
        return result;
    }

    public static byte[] ToBytes<RW, T>(RW rw, T value)
        where RW : IReadWrite<T>
    {
        using var stream = new MemoryStream();
        using var writer = new BinaryWriter(stream);
        rw.Write(writer, value);
        return stream.ToArray();
    }

    public static byte[] ToBytes<T>(T value)
        where T : IStructuralReadWrite
    {
        using var stream = new MemoryStream();
        using var writer = new BinaryWriter(stream);
        value.WriteFields(writer);
        return stream.ToArray();
    }
}

/// <summary>
/// Interface for types that know how to serialize another type.
/// We auto-generate an implementation of <c>IReadWrite&lt;T&gt;</c> for all
/// types marked with <c>[SpacetimeDB.Type]</c>. For a type <c>T</c>, this implementation
/// is accessible at <c>T.BSATN</c>. The implementation is always a zero-sized struct.
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IReadWrite<T>
{
    /// <summary>
    /// Read a BSATN-encoded value of type T from the reader.
    /// Throws on end-of-stream or if the stream is malformed.
    /// Advances the reader to the first byte past the end of the encoded value.
    /// </summary>
    /// <param name="reader"></param>
    /// <returns></returns>
    T Read(BinaryReader reader);

    /// <summary>
    /// Write a BSATN-encoded value of type T to the writer.
    /// </summary>
    void Write(BinaryWriter writer, T value);

    /// <summary>
    /// Get metadata for this type. Used in module initialization.
    /// </summary>
    /// <param name="registrar"></param>
    /// <returns></returns>
    AlgebraicType GetAlgebraicType(ITypeRegistrar registrar);
}

/// <summary>
/// Serializer for enums.
/// </summary>
/// <typeparam name="T"></typeparam>
public readonly struct Enum<T> : IReadWrite<T>
    where T : struct, Enum
{
    /// <summary>
    /// Map from tag -> value, implemented as an array.
    /// Note: the [Type] macro rejects enums with explicitly set values (see Codegen.Tests),
    /// so this array is guaranteed to be continuous and indexed starting from 0.
    /// </summary>
    private static readonly T[] TagToValue = Enum.GetValues(typeof(T)).Cast<T>().ToArray();

    public T Read(BinaryReader reader)
    {
        var tag = reader.ReadByte();
        try
        {
            return TagToValue[tag];
        }
        catch
        {
            throw new ArgumentOutOfRangeException(
                $"Tag {tag} is out of range of enum {typeof(T).Name}"
            );
        }
    }

    public void Write(BinaryWriter writer, T value)
    {
        // Previously this was: `if (Enum.IsDefined(typeof(T), value))`.
        // This was quite expensive because:
        //   1. It uses reflection
        //   2. It allocates
        //   3. It is called on each row when decoding
        //
        // However, enum values are guaranteed to be sequential and zero based.
        // Hence we only ever need to do an upper bound check.
        // See `SpacetimeDB.Type.ParseEnum` for the syntax analysis.
        //
        // Note: this actually still uses reflection and allocates.
        // It's hard to figure out how to avoid this without custom-generating a writer for each enum type.
        var tag = Convert.ToByte(value);
        if (tag < TagToValue.Length)
        {
            writer.Write(tag);
        }
        else
        {
            throw new ArgumentOutOfRangeException(
                $"Value {value} is out of range for enum {typeof(T).Name}"
            );
        }
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        registrar.RegisterType<T>(
            (_) =>
                new AlgebraicType.Sum(
                    Enum.GetNames(typeof(T))
                        .Select(name => new AggregateElement(name, AlgebraicType.Unit))
                        .ToArray()
                )
        );
}

public readonly struct RefOption<Inner, InnerRW> : IReadWrite<Inner?>
    where Inner : class
    where InnerRW : IReadWrite<Inner>, new()
{
    private static readonly InnerRW innerRW = new();

    public Inner? Read(BinaryReader reader) => reader.ReadBoolean() ? null : innerRW.Read(reader);

    public void Write(BinaryWriter writer, Inner? value)
    {
        writer.Write(value is null);
        if (value is not null)
        {
            innerRW.Write(writer, value);
        }
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        AlgebraicType.MakeOption(innerRW.GetAlgebraicType(registrar));
}

// This implementation is nearly identical to RefOption. The only difference is the constraint on T.
// Yes, this is dumb, but apparently you can't have *really* generic `T?` because,
// despite identical bodies, compiler will desugar it to very different
// types based on whether the constraint makes it a reference type or a value type.
public readonly struct ValueOption<Inner, InnerRW> : IReadWrite<Inner?>
    where Inner : struct
    where InnerRW : IReadWrite<Inner>, new()
{
    private static readonly InnerRW innerRW = new();

    public Inner? Read(BinaryReader reader) => reader.ReadBoolean() ? null : innerRW.Read(reader);

    public void Write(BinaryWriter writer, Inner? value)
    {
        writer.Write(!value.HasValue);
        if (value.HasValue)
        {
            innerRW.Write(writer, value.Value);
        }
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        AlgebraicType.MakeOption(innerRW.GetAlgebraicType(registrar));

    // Return a List BSATN serializer that can serialize this option as an array
    public static List<Inner, InnerRW> GetListSerializer()
    {
        return new List<Inner, InnerRW>();
    }
}

public readonly struct Bool : IReadWrite<bool>
{
    public bool Read(BinaryReader reader) => reader.ReadBoolean();

    public void Write(BinaryWriter writer, bool value) => writer.Write(value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.Bool(default);
}

public readonly struct U8 : IReadWrite<byte>
{
    public byte Read(BinaryReader reader) => reader.ReadByte();

    public void Write(BinaryWriter writer, byte value) => writer.Write(value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.U8(default);
}

public readonly struct U16 : IReadWrite<ushort>
{
    public ushort Read(BinaryReader reader) => reader.ReadUInt16();

    public void Write(BinaryWriter writer, ushort value) => writer.Write(value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.U16(default);
}

public readonly struct U32 : IReadWrite<uint>
{
    public uint Read(BinaryReader reader) => reader.ReadUInt32();

    public void Write(BinaryWriter writer, uint value) => writer.Write(value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.U32(default);
}

public readonly struct U64 : IReadWrite<ulong>
{
    public ulong Read(BinaryReader reader) => reader.ReadUInt64();

    public void Write(BinaryWriter writer, ulong value) => writer.Write(value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.U64(default);
}

public readonly struct U128Stdb : IReadWrite<SpacetimeDB.U128>
{
    public SpacetimeDB.U128 Read(BinaryReader reader)
    {
        var lower = reader.ReadUInt64();
        var upper = reader.ReadUInt64();
        return new(upper, lower);
    }

    public void Write(BinaryWriter writer, SpacetimeDB.U128 value)
    {
        writer.Write(value.Lower);
        writer.Write(value.Upper);
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.U128(default);
}

#if NET7_0_OR_GREATER
public readonly struct U128 : IReadWrite<UInt128>
{
    public UInt128 Read(BinaryReader reader)
    {
        var lower = reader.ReadUInt64();
        var upper = reader.ReadUInt64();
        return new(upper, lower);
    }

    public void Write(BinaryWriter writer, UInt128 value)
    {
        writer.Write((ulong)value);
        writer.Write((ulong)(value >> 64));
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.U128(default);
}
#endif

public readonly struct U256 : IReadWrite<SpacetimeDB.U256>
{
    public SpacetimeDB.U256 Read(BinaryReader reader)
    {
        var bsatn = new U128Stdb();
        var lower = bsatn.Read(reader);
        var upper = bsatn.Read(reader);
        return new(upper, lower);
    }

    public void Write(BinaryWriter writer, SpacetimeDB.U256 value)
    {
        var bsatn = new U128Stdb();
        bsatn.Write(writer, value.Lower);
        bsatn.Write(writer, value.Upper);
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.U256(default);
}

public readonly struct I8 : IReadWrite<sbyte>
{
    public sbyte Read(BinaryReader reader) => reader.ReadSByte();

    public void Write(BinaryWriter writer, sbyte value) => writer.Write(value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.I8(default);
}

public readonly struct I16 : IReadWrite<short>
{
    public short Read(BinaryReader reader) => reader.ReadInt16();

    public void Write(BinaryWriter writer, short value) => writer.Write(value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.I16(default);
}

public readonly struct I32 : IReadWrite<int>
{
    public int Read(BinaryReader reader) => reader.ReadInt32();

    public void Write(BinaryWriter writer, int value) => writer.Write(value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.I32(default);
}

public readonly struct I64 : IReadWrite<long>
{
    public long Read(BinaryReader reader) => reader.ReadInt64();

    public void Write(BinaryWriter writer, long value) => writer.Write(value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.I64(default);
}

public readonly struct I128Stdb : IReadWrite<SpacetimeDB.I128>
{
    public SpacetimeDB.I128 Read(BinaryReader reader)
    {
        var lower = reader.ReadUInt64();
        var upper = reader.ReadUInt64();
        return new(upper, lower);
    }

    public void Write(BinaryWriter writer, SpacetimeDB.I128 value)
    {
        writer.Write(value.Lower);
        writer.Write(value.Upper);
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.I128(default);
}

#if NET7_0_OR_GREATER
public readonly struct I128 : IReadWrite<Int128>
{
    public Int128 Read(BinaryReader reader)
    {
        var lower = reader.ReadUInt64();
        var upper = reader.ReadUInt64();
        return new(upper, lower);
    }

    public void Write(BinaryWriter writer, Int128 value)
    {
        writer.Write((long)value);
        writer.Write((long)(value >> 64));
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.I128(default);
}
#endif

public readonly struct I256 : IReadWrite<SpacetimeDB.I256>
{
    public SpacetimeDB.I256 Read(BinaryReader reader)
    {
        var bsatn = new U128Stdb();
        var lower = bsatn.Read(reader);
        var upper = bsatn.Read(reader);
        return new(upper, lower);
    }

    public void Write(BinaryWriter writer, SpacetimeDB.I256 value)
    {
        var bsatn = new U128Stdb();
        bsatn.Write(writer, value.Lower);
        bsatn.Write(writer, value.Upper);
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.I256(default);
}

public readonly struct F32 : IReadWrite<float>
{
    public float Read(BinaryReader reader) => reader.ReadSingle();

    public void Write(BinaryWriter writer, float value) => writer.Write(value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.F32(default);
}

public readonly struct F64 : IReadWrite<double>
{
    public double Read(BinaryReader reader) => reader.ReadDouble();

    public void Write(BinaryWriter writer, double value) => writer.Write(value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.F64(default);
}

readonly struct Enumerable<Element, ElementRW> : IReadWrite<IEnumerable<Element>>
    where ElementRW : IReadWrite<Element>, new()
{
    private static readonly ElementRW elementRW = new();

    public IEnumerable<Element> Read(BinaryReader reader)
    {
        var count = reader.ReadInt32();
        for (var i = 0; i < count; i++)
        {
            yield return elementRW.Read(reader);
        }
    }

    public void Write(BinaryWriter writer, IEnumerable<Element> value)
    {
        writer.Write(value.Count());
        foreach (var element in value)
        {
            elementRW.Write(writer, element);
        }
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.Array(elementRW.GetAlgebraicType(registrar));
}

public readonly struct Array<Element, ElementRW> : IReadWrite<Element[]>
    where ElementRW : IReadWrite<Element>, new()
{
    private static readonly Enumerable<Element, ElementRW> enumerable = new();
    private static readonly ElementRW elementRW = new();

    public Element[] Read(BinaryReader reader)
    {
        // Don't use Enumerable here: save an allocation and pre-allocate the output.
        var count = reader.ReadInt32();
        var result = new Element[count];
        for (var i = 0; i < count; i++)
        {
            result[i] = elementRW.Read(reader);
        }
        return result;
    }

    public void Write(BinaryWriter writer, Element[] value) => enumerable.Write(writer, value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        enumerable.GetAlgebraicType(registrar);
}

// Special case for byte arrays that can be dealt with more efficiently.
public readonly struct ByteArray : IReadWrite<byte[]>
{
    public static readonly ByteArray Instance = new();

    public byte[] Read(BinaryReader reader) => reader.ReadBytes(reader.ReadInt32());

    public void Write(BinaryWriter writer, byte[] value)
    {
        writer.Write(value.Length);
        writer.Write(value);
    }

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.Array(new AlgebraicType.U8(default));
}

// String is a special case of byte array with extra checks.
public readonly struct String : IReadWrite<string>
{
    public string Read(BinaryReader reader) =>
        Encoding.UTF8.GetString(ByteArray.Instance.Read(reader));

    public void Write(BinaryWriter writer, string value) =>
        ByteArray.Instance.Write(writer, Encoding.UTF8.GetBytes(value));

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        new AlgebraicType.String(default);
}

public readonly struct List<Element, ElementRW> : IReadWrite<List<Element>>
    where ElementRW : IReadWrite<Element>, new()
{
    private static readonly Enumerable<Element, ElementRW> enumerable = new();
    private static readonly ElementRW elementRW = new();

    public List<Element> Read(BinaryReader reader)
    {
        // Don't use Enumerable here: save an allocation and pre-allocate the output.
        var count = reader.ReadInt32();
        var result = new List<Element>(count);
        for (var i = 0; i < count; i++)
        {
            result.Add(elementRW.Read(reader));
        }
        return result;
    }

    public void Write(BinaryWriter writer, List<Element> value) => enumerable.Write(writer, value);

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
        enumerable.GetAlgebraicType(registrar);
}

// This is a dummy type, mainly used by codegen as a diagnostics placeholder to
// reduce amount of noisy compilation errors when a used type is not supported by BSATN.
public readonly struct Unsupported<T> : IReadWrite<T>
{
    private static readonly NotSupportedException Exception =
        new($"Type {typeof(T)} is not supported by BSATN.");

    public T Read(BinaryReader reader) => throw Exception;

    public void Write(BinaryWriter writer, T value) => throw Exception;

    public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => throw Exception;
}

/// <summary>
/// Support methods for converting <c>[SpacetimeDB.Type]</c>s to strings.
/// </summary>
public static class StringUtil
{
    /// <summary>
    /// Convert an arbitrary object to a string:
    /// - Printing <c>null</c> instead of empty string for null objects
    /// - Quoting strings
    /// - Printing list contents as <c>$"[ {list[0].ToString()} {list[1].ToString()} {...} {list[n-1].ToString()} ]"</c>,
    ///     printing at most 16 elements of the list, with an ellipsis in the middle if there
    ///     are more. (This is to prevent crashing Unity if you accidentally print a large array, say.)
    ///
    /// This is NOT a deep pretty-printer: it only pretty-prints the object given, relying on <c>ToString()</c>
    /// to print sub-objects. However, objects marked with <c>[SpacetimeDB.Type]</c> use this method as part of
    /// generated code to implement deep pretty-printing in their <c>ToString()</c> implementations.
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static string GenericToString(object? obj)
    {
        if (obj == null)
        {
            return "null";
        }

        var str = obj as string;
        if (str != null)
        {
            return ToStringLiteral(str);
        }

        // Casting to IList means if a user implements IList for some
        // [SpacetimeDB.Type], it will get printed as a list.
        // Shrug.
        var list = obj as System.Collections.IList;
        if (list != null)
        {
            return GenericListToString(list);
        }

        return obj.ToString()!;
    }

    internal static string ToStringLiteral(string input)
    {
        var literal = new StringBuilder(input.Length + 2);
        literal.Append('\"');
        foreach (var c in input)
        {
            switch (c)
            {
                case '\"':
                    literal.Append("\\\"");
                    break;
                case '\\':
                    literal.Append(@"\\");
                    break;
                case '\0':
                    literal.Append(@"\0");
                    break;
                case '\a':
                    literal.Append(@"\a");
                    break;
                case '\b':
                    literal.Append(@"\b");
                    break;
                case '\f':
                    literal.Append(@"\f");
                    break;
                case '\n':
                    literal.Append(@"\n");
                    break;
                case '\r':
                    literal.Append(@"\r");
                    break;
                case '\t':
                    literal.Append(@"\t");
                    break;
                case '\v':
                    literal.Append(@"\v");
                    break;
                default:
                    if (c is >= (char)0x20 and <= (char)0x7e)
                    {
                        // ASCII printable character
                        literal.Append(c);
                    }
                    else if (
                        Char.GetUnicodeCategory(c) == System.Globalization.UnicodeCategory.Control
                    )
                    {
                        // As UTF16 escaped character
                        literal.Append(@"\u");
                        literal.Append(((int)c).ToString("x4"));
                    }
                    else
                    {
                        // Something else
                        literal.Append(c);
                    }
                    break;
            }
        }
        literal.Append('"');
        return literal.ToString();
    }

    internal static string GenericListToString(System.Collections.IList list)
    {
        StringBuilder result = new();
        result.Append("[ ");

        // avoid debug-dumping huge lists.
        if (list.Count <= 16)
        {
            for (var i = 0; i < list.Count; i++)
            {
                result.Append(GenericToString(list[i]));
                if (i < list.Count - 1)
                {
                    result.Append(", ");
                }
            }
        }
        else
        {
            for (var i = 0; i < 8; i++)
            {
                result.Append(GenericToString(list[i]));
                result.Append(", ");
            }
            result.Append("..., ");
            for (var i = list.Count - 8; i < list.Count; i++)
            {
                result.Append(GenericToString(list[i]));
                if (i < list.Count - 1)
                {
                    result.Append(", ");
                }
            }
        }

        if (list.Count > 0)
        {
            result.Append(' ');
        }
        result.Append(']');

        return result.ToString();
    }
}
